// author: moonxi
export class FlipDrag {
  // 相关配置
  options = {
    _duration: 500, // 调节 FLIP 动画的持续时间
    _position: 'relative',
    transition: 'all 500ms ease',
    dynamic: true,
    set duration(value) {
      this.transition = `all ${value}ms ease`
      this._duration = value
    },
    get duration() {
      return this._duration
    },
    set position(value) {
      if (value === 'absolute' || value === 'relative' || value === 'fixed' || value === 'sticky') {
        this._position = value
      }
    },
    get position() {
      return this._position
    }
  }

  #draggableEles = new WeakMap() // 存储可拖拽元素
  #observer = null // 动态监听的观察者对象
  #dynamic = false // 是否动态监听
  #isDragged = false // 是否拖拽过
  #childrenSnapShot = [] // 子元素的快照

  set dynamic(value) {
    this.#dynamic = value
    this.options.dynamic = value
    if (value) {
      this.#injectDynamic()
    } else {
      this.#removeDymamic()
    }
  }
  get dynamic() {
    return this.#dynamic
  }

  // 读取容器下的所有子元素
  get children() {
    return Array.from(this.container.children)
  }

  get isDragged() {
    return this.#isDragged
  }

  // 读取容器的位置信息
  get containerRect() {
    return this.container.getBoundingClientRect()
  }
  constructor(container, options = {}) {
    this.container = container
    // 初始化配置
    Object.keys(options).forEach(key => {
      if (this.options.hasOwnProperty(key)) {
        this.options[key] = options[key]
      }
    })
    this.init()
  }

  // 初始化函数
  init() {
    // 为每个子元素添加拖拽效果
    this.children.forEach(item => {
      this.#addDrag(item)
    })
    this.container.style.position = this.options.position
    this.dynamic = this.options.dynamic
  }

  // 防抖函数（扩展用）
  #debounce(fn, delay) {
    let timer
    return function (...args) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  }

  // 节流函数（扩展用）
  #throttle(fn, delay, immediate = false) {
    let start = Date.now()
    return function (...args) {
      const now = Date.now()
      if (now - start > delay || immediate) {
        fn.apply(this, args)
        start = now
        immediate = false
      }
    }
  }

  // 判断是否是触摸事件
  #isTouch(event) {
    return event.type.includes('touch')
  }

  // 获取元素的位置信息
  #getPositon(ele) {
    const rect = ele.getBoundingClientRect()
    return {
      x: rect.left,
      y: rect.top
    }
  }

  // 计算两个点的位移
  #calMovement(p1, p2) {
    return {
      deltaX: p1.x - p2.x,
      deltaY: p1.y - p2.y
    }
  }

  // 注入动态监听子节点
  #injectDynamic() {
    if (this.#observer) return
    const observer = new MutationObserver(mutations => {
      this.children.forEach(item => {
        this.#addDrag(item)
      })
    })
    this.#observer = observer
    observer.observe(this.container, {
      childList: true
    })
  }

  // 移除动态监听子节点
  #removeDymamic() {
    if (this.#observer) {
      this.#observer.disconnect()
      this.#observer = null
    }
  }

  // 为元素添加 FLIP 效果
  #flip(ele) {
    if (ele.disableDrag) return () => Promise.resolve(true)
    const firstPos = this.#getPositon(ele)
    return () => {
      const p = new Promise((resolve, reject) => {
        const lastPos = this.#getPositon(ele)
        const movement = this.#calMovement(firstPos, lastPos)
        ele.style.transition = ''
        ele.style.transform = `translate(${movement.deltaX}px, ${movement.deltaY}px)`
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            ele.style.transition = this.options.transition
            ele.style.transform = ''
          })
        })
        const transitionEnd = () => {
          ele.removeEventListener('transitionend', transitionEnd)
          ele.style.transition = ''

          resolve(true)
        }
        ele.addEventListener('transitionend', transitionEnd)
      })
      return p
    }
  }

  // 为元素添加拖拽效果
  #addDrag(ele) {
    if (this.#draggableEles.has(ele) || ele.disableDrag) return
    ele.draggable = true
    ele.style.position = 'relative'
    let startX = 0
    let startY = 0

    const cloneEle = ele.cloneNode(true)
    ele.__cloneEle = cloneEle
    cloneEle.disableDrag = true
    cloneEle.style.position = 'fixed'

    let touchstartX = 0
    let touchstartY = 0
    cloneEle.style.opacity = 0.5

    const placeholder = ele.cloneNode(true)
    placeholder.disableDrag = true
    let placeholderRect = null

    let items = []
    let rects = []

    // 拖拽元素核心算法
    const _locateEle = event => {
      const isTouch = this.#isTouch(event)
      const clientX = isTouch ? event.changedTouches[0].clientX : event.clientX
      const clientY = isTouch ? event.changedTouches[0].clientY : event.clientY

      const __calcDistance = rect => {
        const deltaY = rect.y - clientY + startY
        const deltaX = rect.x - clientX + startX
        const distance = deltaX * deltaX + deltaY * deltaY
        return distance
      }
      const distances = rects.map((rect, index) => {
        const distance = __calcDistance(rect)
        return {
          distance,
          index
        }
      })
      distances.push({ distance: __calcDistance(placeholderRect), index: items.length })
      distances.sort((a, b) => a.distance - b.distance)

      const i = this.children.indexOf(ele)
      const index = distances[0].index
      if (i === index) return
      const plays = items.map(item => this.#flip(item))
      if (index === items.length) {
        this.container.appendChild(ele)
      } else {
        this.container.insertBefore(ele, items[index])
      }
      plays.forEach(play => play())
    }

    // 开始拖拽
    const start = event => {
      this.#childrenSnapShot = this.children

      const isTouch = this.#isTouch(event)
      const pos = this.#getPositon(ele)
      if (isTouch) {
        startX = event.changedTouches[0].clientX - pos.x
        startY = event.changedTouches[0].clientY - pos.y
        touchstartX = event.changedTouches[0].clientX - pos.x
        touchstartY = event.changedTouches[0].clientY - pos.y
      } else {
        startX = event.clientX - pos.x
        startY = event.clientY - pos.y
      }
      ele.style.opacity = 0.3

      items = this.children.filter(item => item !== ele && !item.disableDrag)
      rects = items.map(item => item.getBoundingClientRect())

      this.container.appendChild(placeholder)
      placeholderRect = placeholder.getBoundingClientRect()
      placeholder.remove()
    }

    // 拖拽中
    const move = event => {
      _locateEle(event)
    }

    // 结束拖拽
    const end = event => {
      const isTouch = this.#isTouch(event)
      _locateEle(event)
      const pos = this.#getPositon(ele)
      ele.style.left =
        (isTouch ? event.changedTouches[0].clientX : event.clientX) - pos.x - startX + 'px'
      ele.style.top =
        (isTouch ? event.changedTouches[0].clientY : event.clientY) - pos.y - startY + 'px'
      const play = this.#flip(ele)
      ele.style.opacity = 1
      ele.style.left = 'auto'
      ele.style.top = 'auto'
      play()
      for (let i = 0; i < this.#childrenSnapShot.length; i++) {
        if (this.#childrenSnapShot[i] !== this.children[i]) {
          this.#isDragged = true
          break
        }
      }
    }
    let timer = null
    let touchDrag = false
    const clearTimer = () => {
      clearTimeout(timer)
    }
    const touchStart = e => {

      if (e.target.tagName === 'use' || e.target.tagName === 'IMG') {
        ele.removeEventListener('dragstart', start)
        ele.removeEventListener('drag', move)
        ele.removeEventListener('dragend', end)
        return
      }

      window.addEventListener('touchmove', clearTimer)
      timer = setTimeout(() => {
        e.preventDefault()
        touchDrag = true
        const rect = ele.getBoundingClientRect()
        cloneEle.style.width = rect.width + 'px'
        cloneEle.style.height = rect.height + 'px'
        cloneEle.style.left = rect.x + 'px'
        cloneEle.style.top = rect.y + 'px'
        this.container.appendChild(cloneEle)
        start(e)

        ele.addEventListener('touchmove', touchMove)
      }, 500)
    }
    const touchMove = e => {
      e.preventDefault()

      cloneEle.style.left = e.changedTouches[0].clientX - touchstartX + 'px'
      cloneEle.style.top = e.changedTouches[0].clientY - touchstartY + 'px'
      move(e)
    }
    const touchEnd = e => {
      clearTimeout(timer)
      window.removeEventListener('touchmove', clearTimer)
      ele.removeEventListener('touchmove', touchMove)
      cloneEle.remove()

      ele.addEventListener('dragstart', start)
      ele.addEventListener('drag', move)
      ele.addEventListener('dragend', end)
      if (touchDrag) {
        end(e)
        touchDrag = false
      }
    }

    ele.addEventListener('dragstart', start)
    ele.addEventListener('drag', move)
    ele.addEventListener('dragend', end)

    ele.addEventListener('touchstart', touchStart)

    ele.addEventListener('touchend', touchEnd)
    ele.addEventListener('touchcancel', touchEnd)

    // 移除拖拽效果
    const removeDrag = () => {
      ele.draggable = false
      ele.removeEventListener('dragstart', start)
      ele.removeEventListener('drag', move)
      ele.removeEventListener('dragend', end)

      ele.removeEventListener('touchstart', touchStart)
      ele.removeEventListener('touchmove', touchMove)
      ele.removeEventListener('touchend', touchEnd)

      this.#draggableEles.delete(ele)
    }
    this.#draggableEles.set(ele, removeDrag)
  }

  // 向容器中添加元素
  add(ele, index) {
    this.#addDrag(ele)
    const plays = this.children.map(item => this.#flip(item))
    this.container.insertBefore(ele, this.children[index])
    plays.forEach(play => play())
  }

  // 移除容器中的元素
  remove(index) {
    const ele = this.children[index]
    const plays = this.children.filter(item => item !== ele).map(item => this.#flip(item))
    ele.remove()
    plays.forEach(play => play())
  }

  // 按照排序函数对容器中的元素进行排序
  sort(fn) {
    const plays = this.children.map(item => this.#flip(item))
    const items = this.children.sort(fn)
    for (let i = 0; i < items.length; i++) {
      this.container.insertBefore(items[i], this.children[i])
    }
    plays.forEach(play => play())
  }
  flip() {
    const plays = this.children.map(item => this.#flip(item))
    return () => {
      const promises = []
      plays.forEach(play => promises.push(play()))
      const p = Promise.all(promises)
      return p
    }
  }
}
